#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "crypto/types.h"
#include "crypto/tool.h"
#include "crypto/errors.h"
#include "crypto/bignum.h"

static void BNpurtyadd(BN *augend, BN *addend, BN *sum)
{
    int len;
    int cap;
    bool isCount = false;
    int index = 0, tmp;

    len = augend->len > addend->len ? augend->len : addend->len;
    cap = (len + 4) / 4 * 4;

    if (sum->cap < cap)
    {
        if (sum->num != NULL)
        {
            free(sum->num);
        }
        sum->cap = cap;
        sum->num = (byte *)malloc(cap);
    }

    while (index < augend->len && index < addend->len)
    {
        tmp = (augend->num[index] & 15) + (addend->num[index] & 15);

        if (isCount)
        {
            tmp++;
        }

        if (tmp >= 10)
        {
            isCount = true;
            tmp %= 10;
        }
        else
        {
            isCount = false;
        }

        sum->num[index] = tmp | 48;

        index++;
    }

    while (index < augend->len)
    {
        tmp = augend->num[index] & 15;

        if (isCount)
        {
            tmp++;
        }

        if (tmp >= 10)
        {
            isCount = true;
            tmp %= 10;
        }
        else
        {
            isCount = false;
        }

        sum->num[index] = tmp | 48;
        index++;
    }

    while (index < addend->len)
    {

        tmp = addend->num[index] & 15;

        if (isCount)
        {
            tmp++;
        }

        if (tmp >= 10)
        {
            isCount = true;
            tmp %= 10;
        }
        else
        {
            isCount = false;
        }

        sum->num[index] = tmp | 48;

        index++;
    }

    if (isCount)
    {
        sum->num[index] = '1';
        sum->len = index + 1;
    }
    else
    {
        sum->len = index;
    }
}

static void BNpurtysub(BN *minuend, BN *subtrahend, BN *difference)
{
    int len = minuend->len;
    int cap;
    bool isCount = false;
    int index = 0, tmp;

    cap = (len + 3) / 4 * 4;
    if (difference->cap < cap)
    {
        if (difference->num != NULL)
        {
            free(difference->num);
        }
        difference->cap = cap;
        difference->num = (byte *)malloc(cap);
    }

    while (index < minuend->len && index < subtrahend->len)
    {
        tmp = (minuend->num[index] & 15) - (subtrahend->num[index] & 15);

        if (isCount)
        {
            tmp--;
        }

        if (tmp < 0)
        {
            isCount = true;
            tmp += 10;
        }
        else
        {
            isCount = false;
        }

        difference->num[index] = tmp | 48;

        index++;
    }

    while (index < minuend->len)
    {
        tmp = minuend->num[index] & 15;

        if (isCount)
        {
            tmp--;
        }

        if (tmp < 0)
        {
            isCount = true;
            tmp += 10;
        }
        else
        {
            isCount = false;
        }

        difference->num[index] = tmp | 48;
        index++;
    }

    while (--index)
    {
        if (difference->num[index] != '0')
        {
            break;
        }
    }
    difference->len = index + 1;
    if (difference->len == 0)
    {
        difference->len = 1;
    }
    return;
}

// product is a buffer which is prepared readly to accelerate calculation!
static void BNmultiN(BN *multiplicand, uint n, BN *product)
{
    if (n <= 0 || n >= 10)
    {
        export(CODE_PARAMS_ERROR, "The n is need to between 1~9!");
    }
    int index = 0;
    int count = 0;
    int tmp;
    while (index < multiplicand->len)
    {
        tmp = (multiplicand->num[index] & 15) * n + count;

        count = tmp / 10;
        tmp %= 10;

        product->num[index] = tmp | 48;
        index++;
    }
    if (count != 0)
    {
        if (product->len >= product->cap)
        {
            export(CODE_PARAMS_ERROR, "The buffer don't have enough space!");
        }
        product->num[index] = count | 48;
        index++;
    }
    product->len = index;
}
// product is a buffer which is prepared readly to accelerate calculation!
static void BNmulti10n(BN *multiplicand, uint n, BN *product)
{
    product->len = multiplicand->len + n;
    if (product->len > product->cap)
    {
        export(CODE_PARAMS_ERROR, "The buffer don't have enough space!");
    }
    memset(product->num, 48, n);
    memcpy(product->num + n, multiplicand->num, multiplicand->len);
}

void BNhalf(BN *dividend, BN *quotient, bool *mod)
{
    if (dividend->len == 0)
    {
        BNsetZero(quotient);
        *mod = false;
    }

    if (quotient->cap < dividend->len)
    {
        free(quotient->num);
        quotient->num = (byte *)malloc(dividend->cap);
        quotient->cap = dividend->cap;
    }

    quotient->len = dividend->len;
    quotient->flag = dividend->flag;

    int index = dividend->len;
    bool isCount = false;
    int tmp;
    int val;

    while (--index >= 0)
    {
        tmp = dividend->num[index] & 15;
        if (isCount)
        {
            tmp += 10;
        }
        val = tmp / 2;
        tmp %= 2;

        isCount = tmp == 0 ? false : true;

        quotient->num[index] = val | 48;
    }

    *mod = dividend->num[0] % 2 ? true : false;

    if (dividend->num[dividend->len - 1] == '1' && quotient->len > 1)
    {
        quotient->len--;
    }

    if (quotient->len == 1 && quotient->num[0] == '0')
    {
        quotient->flag = POS;
    }
}

// return empty or 0 big number struct pointer
BN *BNinit(uint len)
{
    BN *bn = (BN *)malloc(sizeof(BN));
    if (len == 0)
    {
        bn->len = 0;
        bn->cap = 0;
        bn->num = NULL;
        bn->flag = ILLEGAL;
    }
    else
    {
        bn->len = 1;
        bn->cap = (len + 3) / 4 * 4;
        bn->num = (byte *)malloc(bn->cap);
        bn->num[0] = '0';
        bn->flag = POS;
    }
    return bn;
}

// return a big number struct pointer which value is equal to addr
BN *BNbuild(byte *addr, uint len)
{
    uint cap;
    BN *bn;
    if (len <= 0)
    {
        len = strlen(addr);
    }
    if (isDigital(*addr))
    {
        bn = (BN *)malloc(sizeof(BN));
        if (bn == NULL)
        {
            export(CODE_OOM, "malloc space for BN failed!");
        }
        while (isZero(*addr) && len)
        {
            addr++;
            len--;
        }
        if (len == 0)
        {
            if (isZero(*(addr - 1)))
            {
                bn->flag = POS;
                bn->len = 1;
                cap = (len + 3) / 4 * 4;
                bn->num = (byte *)malloc(cap);
                bn->num[0] = '0';
                return bn;
            }
            else
            {
                export(CODE_PARAMS_ERROR, "The addr is a bad params!");
            }
        }
        bn->flag = POS;
        bn->len = len;
    }
    else
    {
        if (*addr == '-')
        {
            bn = (BN *)malloc(sizeof(BN));
            addr++;
            len--;
            while (isZero(*addr) && len)
            {
                addr++;
                len--;
            }
            if (len == 0)
            {
                export(CODE_PARAMS_ERROR, "The addr is a bad params!");
            }
            bn->flag = NEG;
            bn->len = len;
        }
        else if (*addr == '+')
        {
            bn = (BN *)malloc(sizeof(BN));
            addr++;
            len--;
            while (isZero(*addr) && len)
            {
                addr++;
                len--;
            }
            if (len == 0)
            {
                export(CODE_PARAMS_ERROR, "The addr is a bad params!");
            }
            bn->flag = POS;
            bn->len = len;
        }
        else
        {
            export(CODE_PARAMS_ERROR, "The start value of addr is either not signal or digital!");
        }
    }
    cap = (len + 3) / 4 * 4;
    bn->num = (byte *)malloc(cap);
    for (int i = 0; i < len; i++)
    {
        if (!isDigital(addr[len - 1 - i]))
        {
            free(bn->num);
            free(bn);
            export(CODE_PARAMS_ERROR, "The addr is not a legal number string!");
        }
        bn->num[i] = addr[len - 1 - i];
    }
    return bn;
}

BN *BNbuildInt(int data)
{
    BN *bn = BNinit(MAX_INT_VALUE_LENGTH);
    int tmp;
    int index = 0;
    if (data < 0)
    {
        bn->flag = NEG;
        data = -data;
    }
    else
    {
        bn->flag = POS;
    }

    while (data != 0)
    {
        tmp = data % 10;
        data /= 10;
        bn->num[index] = tmp | 48;
        index++;
    }

    bn->len = index;
}

BN *BNbuildInt64(int64 data)
{
    BN *bn = BNinit(MAX_LONG_VALUE_LENGTH);
    int tmp;
    int index = 0;
    if (data < 0)
    {
        bn->flag = NEG;
        data = -data;
    }
    else
    {
        bn->flag = POS;
    }

    while (data != 0)
    {
        tmp = data % 10;
        data /= 10;
        bn->num[index] = tmp | 48;
        index++;
    }

    bn->len = index;
}

void BNsetZero(BN *bn)
{
    if (bn->num == NULL)
    {
        bn->num = (byte *)malloc(4);
        bn->cap = 4;
    }

    bn->num[0] = '0';
    bn->len = 1;
}

void BNsetInt(BN *bn, int data)
{
    if (bn->cap < MAX_INT_VALUE_LENGTH)
    {
        if (bn->num == NULL)
        {
            free(bn->num);
        }
        bn->num = (byte *)malloc(MAX_INT_VALUE_LENGTH);
        bn->cap = MAX_INT_VALUE_LENGTH;
    }
    int tmp;
    int index = 0;
    if (data < 0)
    {
        bn->flag = NEG;
        data = -data;
    }
    else
    {
        bn->flag = POS;
    }

    while (data != 0)
    {
        tmp = data % 10;
        data /= 10;
        bn->num[index] = tmp | 48;
        index++;
    }

    bn->len = index;
}

void BNsetInt64(BN *bn, int64 data)
{
    if (bn->cap < MAX_LONG_VALUE_LENGTH)
    {
        if (bn->num == NULL)
        {
            free(bn->num);
        }
        bn->num = (byte *)malloc(MAX_LONG_VALUE_LENGTH);
        bn->cap = MAX_LONG_VALUE_LENGTH;
    }
    int tmp;
    int index = 0;
    if (data < 0)
    {
        bn->flag = NEG;
        data = -data;
    }
    else
    {
        bn->flag = POS;
    }

    while (data != 0)
    {
        tmp = data % 10;
        data /= 10;
        bn->num[index] = tmp | 48;
        index++;
    }

    bn->len = index;
}

void BNadd(BN *augend, BN *addend, BN *sum)
{
    if (augend->flag * addend->flag == ILLEGAL)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }

    if (augend->flag == POS && addend->flag == NEG)
    {
        return BNsub(augend, addend, sum);
    }
    else if (augend->flag == NEG && addend->flag == POS)
    {
        return BNsub(addend, augend, sum);
    }

    BNpurtyadd(augend, addend, sum);
    sum->flag = augend->flag;
}

void BNsub(BN *minuend, BN *subtrahend, BN *difference)
{
    if (minuend->flag * subtrahend->flag == ILLEGAL)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }
    if (minuend->flag != subtrahend->flag)
    {
        if (minuend->flag == POS)
        {
            BNpurtyadd(minuend, subtrahend, difference);
            difference->flag = POS;
        }
        else
        {
            BNpurtyadd(minuend, subtrahend, difference);
            difference->flag = NEG;
        }
    }
    int cmp = BNcompare(minuend, subtrahend);
    if (cmp == POS)
    {
        difference->flag = POS;
        if (minuend->flag == POS)
        {
            BNpurtysub(minuend, subtrahend, difference);
        }
        else
        {
            BNpurtysub(subtrahend, minuend, difference);
        }
    }
    else if (cmp == NEG)
    {
        difference->flag = NEG;
        if (minuend->flag == POS)
        {
            BNpurtysub(subtrahend, minuend, difference);
        }
        else
        {
            BNpurtysub(minuend, subtrahend, difference);
        }
    }
    else
    {
        difference->flag = POS;
        difference->len = 1;
        if (difference->cap < 4)
        {
            difference->cap = 4;
            if (difference->num != NULL)
            {
                free(difference->num);
            }
            difference->num = (byte *)malloc(difference->cap);
        }
        difference->num[0] = '0';
    }
}

bool BNcompare(BN *res, BN *dst)
{
    if (res->flag * dst->flag == ILLEGAL)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }

    if (res->flag != dst->flag)
    {
        return POS == res->flag ? POS : NEG;
    }

    if (res->len != dst->len)
    {
        if (res->len > dst->len)
            return POS == res->flag ? POS : NEG;
        if (res->len < dst->len)
            return NEG == res->flag ? POS : NEG;
    }

    for (int i = res->len - 1; i >= 0; i--)
    {
        if (res->num[i] > dst->num[i])
        {
            return POS == res->flag ? POS : NEG;
        }
        else if (res->num[i] < dst->num[i])
        {
            return NEG == res->flag ? POS : NEG;
        }
    }

    return EQUAL;
}

void BNmulti(BN *multiplicand, BN *multiplier, BN *product)
{
    if (multiplicand->flag * multiplier->flag == 0)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }

    BN *tmp;
    BN *multiBuffer1, *multiBuffer2, *addbuffer;
    int index = 0;
    int len = multiplicand->len + multiplier->len + 2;
    int cap = (len + 3) / 4 * 4;

    if (product->cap < cap)
    {
        if (product->num != NULL)
        {
            free(product->num);
        }
        product->num = (byte *)malloc(cap);
        product->cap = cap;
    }

    multiBuffer1 = BNinit(len);
    multiBuffer2 = BNinit(len);
    addbuffer = BNinit(len);

    if (multiplicand->len < multiplier->len)
    {
        tmp = multiplicand;
        multiplicand = multiplier;
        multiplier = tmp;
    }

    while (index < multiplier->len)
    {
        BNmultiN(multiplicand, multiplier->num[index] & 15, multiBuffer1);
        if (index > 0)
        {
            BNmulti10n(multiBuffer1, index, multiBuffer2);
            BNpurtyadd(addbuffer, multiBuffer2, product);
            BNcopy(product, addbuffer);
        }
        else
        {
            BNcopy(multiBuffer1, addbuffer);
            BNcopy(addbuffer, product);
        }
        index++;
    }

    if (multiplicand->flag * multiplier->flag != POS)
    {
        product->flag = NEG;
    }
    else
    {
        product->flag = POS;
    }

    BNdestroy(multiBuffer1);
    BNdestroy(multiBuffer2);
    BNdestroy(addbuffer);
}

void BNpow(BN *base, BN *exponent, BN *power)
{
    if (base->flag * exponent->flag == 0)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }

    int len = base->len * exponent->len;

    if (exponent->len == 1 && exponent->num[0] == '0')
    {
        BNsetInt(power, 1);
        return;
    }
    else if (base->len == 1 && base->num[0] == '0')
    {
        if (exponent->len == 1 && exponent->num[0] == '0')
        {
            BNsetInt(power, 1);
            return;
        }
        else
        {
            BNsetInt(power, 0);
            return;
        }
    }

    bool mod;
    BN *half = BNinit(exponent->len);
    BN *powBuffer = BNinit(len);
    BNhalf(exponent, half, &mod);

    BNpow(base, half, powBuffer);

    BNmulti(powBuffer, powBuffer, power);

    if (mod)
    {
        BNcopy(powBuffer, power);
        BNmulti(powBuffer, base, power);
    }

    BNdestroy(half);
    BNdestroy(powBuffer);
}

void BNcopy(BN *res, BN *dst)
{
    if (res->flag == ILLEGAL)
    {
        export(CODE_PARAMS_ERROR, "The param don't have a value!");
    }

    if (dst->cap < res->len)
    {
        if (dst->num == NULL)
        {
            free(dst->num);
        }
        dst->num = (byte *)malloc(res->cap);
    }
    memcpy(dst, res, (int)(offset(BN, num)));
    memcpy(dst->num, res->num, res->len);
}

void BNdestroy(BN *bn)
{
    if (bn == NULL)
    {
        return;
    }
    if (bn->num != NULL)
    {
        free(bn->num);
    }
    free(bn);
}

#ifndef PRODUCT
void BNprint(BN *bn)
{
    if (bn->flag == NEG)
    {
        printf("-");
    }
    for (int i = bn->len - 1; i >= 0; i--)
    {
        printf("%c", bn->num[i]);
    }
    printf("\n");
#ifdef PRINT_BIG_NUM_DETAIL
    printf("len: %d\n", bn->len);
#endif
}
#endif